React ADSR Envelope UI with SVG
There's probably a better way to build this with html <canvas>
but I'm opting to build this out with <intput type="range"
and <svg>
and ReactJS.
This is a draft
What you see here is an unfinished component. This post will be updated as I build out this component more.
Pre Planning
The useReducer
hook will be perfect for this as I'll need to reference and update many nested objects in multiple places.
Component
Here is a start of my component
import React, { useReducer, useState } from "react";
import styles from '../styles/env.module.scss'
type ValueType = 'attack'|'decay'|'sustain'|'release'
type Props = {
prop?:string
}
type EnvPoint = {
time:number,
x:number,
y:number,
}
type EnvState = {
attack:EnvPoint,
decay:EnvPoint,
sustain:EnvPoint,
release:EnvPoint,
}
type Action =
| { type: 'RESET' }
| { type: 'SET_VALUE'; payload: {
time:number,
type:ValueType,
x:number,
y:number,
}}
const initialState:EnvState = {
attack: {
time: 10,
x: 0,
y: 10,
},
decay: {
time: 20,
x: 0,
y: 50,
},
sustain: {
time: 50,
x: 0,
y: 50,
},
release: {
time: 10,
x: 0,
y: 90,
},
};
const reducer = (state:EnvState, action:Action) => {
switch (action.type) {
case "SET_VALUE":
return {
...state,
[action.payload.type]: {
time: action.payload.time,
x: action.payload.x,
y: action.payload.y
}
}
case "RESET":
return initialState
default:
return state;
}
};
export function EnvelopeLFOEditor ({ prop }:Props) {
const [state, dispatch] = useReducer(reducer, initialState);
return <div>
<svg className={styles.env} width="100" height="100">
<line
x1={0} y1={100}
x2={state.attack.x} y2={state.attack.y}
stroke="green"
/>
<circle
cx={state.attack.x}
cy="10" r="8"
stroke="green" strokeWidth="2" fill="lime"
/>
<line
x1={state.attack.x} y1={state.attack.y}
x2={state.decay.x} y2={state.decay.y}
stroke="green"
/>
<circle
cx={state.decay.x}
cy={state.decay.y}
r="8"
stroke="green" strokeWidth="2" fill="lime"
/>
<line
x1={state.decay.x} y1={state.decay.y}
x2={state.sustain.x} y2={state.sustain.y}
stroke="green"
/>
<circle
cx={state.sustain.x}
cy="50"
r="8"
stroke="green" strokeWidth="2" fill="lime"
/>
<line
x1={state.sustain.x} y1={state.sustain.y}
x2={state.release.x} y2={state.release.y}
stroke="green"
/>
<circle
cx={state.release.x}
cy="90"
r="8"
stroke="green" strokeWidth="2" fill="lime"
/>
</svg>
<div className={styles.controls}>
<label htmlFor="attack" className={styles.slider} >
<span> attack: {state.attack.time} </span>
<input
name="attack" id="attack"
type="range" min="0" max="100"
value={state.attack.time}
onChange={(e) => dispatch({type: 'SET_VALUE',
payload: {
type: 'attack',
time: Number(e.target.value),
x: Number(e.target.value),
y: state.attack.y,
}
})}
/>
</label>
<label htmlFor="decay" className={styles.slider} >
<span> decay: {state.decay.time} </span>
<input
name="decay" id="decay"
type="range" min="0" max="100"
value={state.decay.time}
onChange={(e) => dispatch({type: 'SET_VALUE',
payload: {
type: 'decay',
time: Number(e.target.value),
x: Number(e.target.value) + state.attack.time,
y: state.decay.y,
}
})}
/>
</label>
<label htmlFor="sustain" className={styles.slider} >
<span> sustain: {state.sustain.time} </span>
<input
name="sustain" id="sustain"
type="range" min="0" max="100"
value={state.sustain.time}
onChange={(e) => dispatch({type: 'SET_VALUE',
payload: {
type: 'sustain',
time: Number(e.target.value),
x: Number(e.target.value) + (state.attack.time + state.decay.time),
y: state.sustain.y,
}
})}
/>
</label>
<label htmlFor="release" className={styles.slider} >
<span> release: {state.release.time} </span>
<input
name="release" id="release"
type="range" min="0" max="100"
value={state.release.time}
onChange={(e) => dispatch({type: 'SET_VALUE',
payload: {
type: 'release',
time: Number(e.target.value),
x: Number(e.target.value) + (state.attack.time + state.decay.time + state.sustain.time),
y: state.release.y,
}
})}
/>
</label>
</div>
<h4> state obj </h4>
{JSON.stringify(state, null, 2)}
</div>
}